/*
  S.M.A.C.K - An operating system kernel
  Copyright (C) 2010,2011 Mattias Holm and Kristian Rietveld
  For licensing and a full list of authors of the kernel, see the files
  COPYING and AUTHORS.
*/

#include <vfs/vfs.h>
#include <vfs/fat16.h>
#include <vfs/wfs.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <bootparam.h>
#include <panic.h>
#include <stdlib.h> // For calloc of arrays
#include <slaballoc.h>
#include <assert.h>
#include <errno.h>

#define MAX_FS 8
#define MAX_OPEN_FILES 32

static vfs_filesys_t *filesys[MAX_FS];
static unsigned fscount;
static vfs_vnode_t *root;

static slab_t vnode_slab;
static slab_t file_slab;


extern void devfs_init(void);

static vfs_vnode_t *vfs_getnode_from_directory(vfs_vnode_t *parent,
                                               const char *name);


void
vfs_init(void)
{
  vnode_slab = slab_create(sizeof(vfs_vnode_t));
  file_slab = slab_create(sizeof(vfs_file_t));

  fat16_init();
  wfs_init();
  devfs_init();
}

void
vfs_mount_root(void)
{
  int len, vlen;
  const char *parameter, *value;

  parameter = bootparam_get_parameter("root", &len);
  if (!parameter)
    return;

  value = bootparam_get_value("dev", &vlen, parameter, len);
  if (!value)
    return;

  /* FIXME: resolve the device name through devfs? */

  if (vlen > 2 && !strncmp(value, "rd", 2))
    {
      int minor = value[2] - 0x30;
      vfs_vnode_t *root;
      vfs_filesys_t *fs;

      if (minor < 0 || minor > 9)
        return;

      /* FIXME: guess filesystem type by super block scanning,
       * also provide an option to specify file system type in
       * boot parameters.  For now, we hardcode WFS.
       */

      fs = vfs_getfilesys("WFS");
      if (!fs)
        {
          puts("error: Could not find filesystem type WFS.");
          puts("Could not mount root filesystem");
          panic();
          return;
        }

      printf("  Mounting root filesystem rd%d, type WFS ...\r\n", minor);

      /* Create VFS root and mount root filesystem */
      root = vfs_mknode();
      root->parent = NULL;
      vfs_mount(root, fs, dev_find(DEV_RAMDISK), minor + 1);
      vfs_setroot(root);
    }
}

vfs_filesys_t*
vfs_getfilesys(const char *fsname)
{
  for (int i = 0 ; i < fscount ; i++) {
    if (!strbcmp(fsname, filesys[i]->fsname)) {
      return filesys[i];
    }
  }
  return NULL;
}

void
vfs_register(vfs_filesys_t *fs)
{
  if (fscount < MAX_FS) filesys[fscount++] = fs;
  else panic();
}


void
vfs_mount(vfs_vnode_t *mountpoint, vfs_filesys_t *fs, dev_t*dev, dev_minor_t minor)
{
  mountpoint->type = VFS_MOUNT;
  mountpoint->special.mount.fs = fs;
  mountpoint->special.mount.dev = dev;
  mountpoint->special.mount.minor = minor;

  fs->mount(fs, mountpoint, dev, minor);

  mountpoint->special.mount.root = fs->root(mountpoint);
}

/* Proxy between VFS file ops and device ops */
static size_t
vfs_dev_file_read(vfs_file_t *file, void *buff, size_t len)
{
  if (file->vnode->type != VFS_DEVICE)
    return -EINVAL;

  return dev_read(file->vnode->special.dev.dev,
                  file->vnode->special.dev.minor,
                  buff, len);
}

static size_t
vfs_dev_file_write(vfs_file_t *file, const void *buff, size_t len)
{
  if (file->vnode->type != VFS_DEVICE)
    return -EINVAL;

  return dev_write(file->vnode->special.dev.dev,
                   file->vnode->special.dev.minor,
                   buff, len);
}

static int
vfs_dev_file_open(vfs_vnode_t *vnode)
{
  if (vnode->type != VFS_DEVICE)
    return -EINVAL;

  return dev_open(vnode->special.dev.dev, vnode->special.dev.minor);
}

static int
vfs_dev_file_close(vfs_file_t *file)
{
  if (file->vnode->type != VFS_DEVICE)
    return -EINVAL;

  return dev_close(file->vnode->special.dev.dev,
                   file->vnode->special.dev.minor);
}

static int
vfs_dev_file_ioctl(vfs_file_t *file, uint32_t request, uint32_t param)
{
  if (file->vnode->type != VFS_DEVICE)
    return -EINVAL;

  return dev_ioctl(file->vnode->special.dev.dev,
                   file->vnode->special.dev.minor,
                   request, param);
}

static int
vfs_dev_file_seek(vfs_file_t *file, off_t offset, int whence)
{
  if (file->vnode->type != VFS_DEVICE)
    return -EINVAL;

  return dev_seek(file->vnode->special.dev.dev,
                  file->vnode->special.dev.minor,
                  offset, whence);
}

static vfs_fileops_t vfs_dev_fileops =
{
  .read = vfs_dev_file_read,
  .write = vfs_dev_file_write,
  .open = vfs_dev_file_open,
  .close = vfs_dev_file_close,
  .ioctl = vfs_dev_file_ioctl,
  .seek = vfs_dev_file_seek
};

vfs_vnode_t*
vfs_mkdev(vfs_vnode_t *parent, const char *name, dev_t *dev, dev_minor_t minor)
{
  vfs_vnode_t *devnode = vfs_mknode();
  devnode->parent = parent;
  strlcpy (devnode->name, name, 63); /* FIXME: Have malloc() instead? */

  devnode->type = VFS_DEVICE;
  devnode->special.dev.dev = dev;
  devnode->special.dev.minor = minor;
  devnode->fops = &vfs_dev_fileops;
  return devnode;
}

void
vfs_setroot(vfs_vnode_t *new_root)
{
  root = new_root;
}


vfs_vnode_t*
vfs_mknode(void)
{
  return slab_alloc(&vnode_slab);
}

void
vfs_releasenode(vfs_vnode_t *vnode)
{
  if (vnode->type == VFS_DIR) {
    if (vnode->special.dir.files_in_dir > 0) return;
  } else if (vnode->type == VFS_MOUNT) {
    if (vnode->special.mount.root != NULL) return;
  }

  slab_free(vnode);
}


vfs_vnode_t**
vfs_getdirp(size_t files)
{
  return calloc(files, sizeof(vfs_vnode_t));
}

void
vfs_releasedirp(vfs_vnode_t *vnode)
{
  assert(vnode->type == VFS_DIR);
  free(vnode->special.dir.files);
}

void
vfs_dirp_addnode(vfs_vnode_t *parent, vfs_vnode_t *node)
{
  int i;
  vfs_vnode_t **dirp;

  if (parent->type != VFS_DIR)
    return;

  if (parent->special.dir.files_in_dir == 0)
    {
      parent->special.dir.files = vfs_getdirp(1);
      parent->special.dir.files[0] = node;
      parent->special.dir.files_in_dir = 1;
      return;
    }

  dirp = vfs_getdirp(parent->special.dir.files_in_dir + 1);
  for (i = 0; i < parent->special.dir.files_in_dir; i++)
    dirp[i] = parent->special.dir.files[i];
  dirp[parent->special.dir.files_in_dir] = node;

  parent->special.dir.files_in_dir++;
  vfs_releasedirp(parent);
  parent->special.dir.files = dirp;
}

void
vfs_dirp_removenode(vfs_vnode_t *parent, vfs_vnode_t *node)
{
  int i, j;
  vfs_vnode_t **dirp;

  if (parent->type != VFS_DIR)
    return;

  if (parent->special.dir.files_in_dir == 1)
    {
      parent->special.dir.files_in_dir = 0;
      vfs_releasedirp(parent);
      parent->special.dir.files = NULL;
      return;
    }

  dirp = vfs_getdirp(parent->special.dir.files_in_dir - 1);

  i = j = 0;
  while (i < parent->special.dir.files_in_dir - 1)
    {
      if (parent->special.dir.files[j] == node)
        {
          j++;
          continue;
        }

      dirp[i] = parent->special.dir.files[j];
      i++;
      j++;
    }

  parent->special.dir.files_in_dir--;
  vfs_releasedirp(parent);
  parent->special.dir.files = dirp;
}


vfs_vnode_t*
vfs_mkdir(vfs_vnode_t *parent, const char *name)
{
  int result;

  if (!parent || !parent->fops->mkdir)
    return NULL;

  result = parent->fops->mkdir(parent, name);
  if (result < 0)
    return NULL;

  return vfs_getnode_from_directory(parent, name);
}

int
vfs_rmdir(vfs_vnode_t *node)
{
  if (!node->fops->rmdir)
    return -1;

  return node->fops->rmdir(node);
}

vfs_vnode_t*
vfs_mkfile(vfs_vnode_t *parent, const char *name)
{
  if (!parent->fops->create)
    return NULL;

  parent->fops->create(parent);
  return NULL; // TODO: Return new vfs node
}

vfs_file_t*
vfs_open(vfs_vnode_t *vnode)
{
  vfs_file_t *file;

  if (!vnode || !vnode->fops->open)
    return NULL;

  if (vnode->fops->open(vnode) < 0)
    return NULL;

  file = slab_alloc(&file_slab);
  file->vnode = vnode;
  file->off = 0;

  return file;
}

vfs_file_t *
vfs_dup(vfs_file_t *file)
{
  vfs_file_t *dup;

  if (!file || !file->vnode)
    return NULL;

  dup = slab_alloc(&file_slab);
  dup->vnode = file->vnode;
  dup->off = file->off;

  /* FIXME: Do we need to properly open this file as well? */

  return dup;
}

void
vfs_close(vfs_file_t *file)
{
  if (!file || !file->vnode || !file->vnode->fops)
    return;

  file->vnode->fops->close(file);

  slab_free(file);
}

vfs_file_t *
vfs_opendir(vfs_vnode_t *vnode)
{
  vfs_file_t *file;

  if (!vnode || vnode->type != VFS_DIR)
    return NULL;

  file = slab_alloc(&file_slab);
  file->vnode = vnode;
  file->off = 0;

  return file;
}

void
vfs_readdir(vfs_file_t *file)
{
  if (!file || !file->vnode || !file->vnode->fops->readdir ||
      file->vnode->type != VFS_DIR)
    return;

  if (file->vnode->special.dir.files)
    return;

  file->vnode->fops->readdir(file->vnode,
                             &file->vnode->special.dir.files_in_dir,
                             &file->vnode->special.dir.files);
}

void
vfs_closedir(vfs_file_t *file)
{
  slab_free(file);
}

size_t
vfs_read(vfs_file_t *file, void *buff, size_t len)
{
  if (!file->vnode->fops->read)
    return -1;

  return file->vnode->fops->read(file, buff, len);
}

size_t
vfs_write(vfs_file_t *file, const void *buff, size_t len)
{
  if (!file->vnode->fops->write)
    return -1;

  return file->vnode->fops->write(file, buff, len);
}

off_t
vfs_seek(vfs_file_t *file, off_t offset, int whence)
{
  if (!file->vnode->fops->seek)
    return -1;

  return file->vnode->fops->seek(file, offset, whence);
}

int
vfs_ioctl(vfs_file_t *file, uint32_t request, uint32_t param)
{
  if (!file->vnode->fops->ioctl)
    return -1;

  return file->vnode->fops->ioctl(file, request, param);
}


static vfs_vnode_t *
vfs_getnode_from_directory(vfs_vnode_t *vnode, const char *filename)
{
  if (vnode->type != VFS_DIR)
    return NULL;

  vfs_vnode_t *file_node = NULL;
  for (int i = 0 ; i < vnode->special.dir.files_in_dir ; i ++) {
      if (!strbcmp(filename, vnode->special.dir.files[i]->name)) {
          file_node = vnode->special.dir.files[i];
          break;
      }
  }

  return file_node;
}

vfs_vnode_t*
vfs_getnode(const char *path)
{
  char filename[VFS_FILENAME_MAX];
  if (path[0] != '/') {
    return NULL;
  }

  vfs_vnode_t *vnode = root;

  while (vnode && path && (path = strchr(path, '/'))) {
    while (*path == '/') path ++; // Ignore path separator
    char *end = strchr(path, '/'); //
    if (!end) {
      int len = strnlen(path, 64);
      if (len > 0)
        end = (char *)&path[len];
      else
        end = NULL;
    }
    strlcpy(filename, path, end-path+1); // Add 1 to compensate for '\0'

    size_t filecount = 0;
    vfs_vnode_t **dir_entries = NULL;

    // If this is a mount node, peek through it and get the mount root
    if (vnode->type == VFS_MOUNT) {
      vnode = vnode->special.mount.root;
    }

    // If we now have a directory, it may be an unresolved directory, ask the filesystem
    // to read the file as a directory, if it is a dir, we cache the results in a virtual
    // directory
    if (vnode->type == VFS_DIR && vnode->special.dir.files == NULL) {
      int res = vnode->fops->readdir(vnode, &filecount, &dir_entries);
      if (res == 0) {
        vnode->type = VFS_DIR;
        vnode->special.dir.files = dir_entries;
        vnode->special.dir.files_in_dir = filecount;
      }
    }

    // May be at the end of the path if the path ends with '/', in that case, the file name is empty
    if (filename[0] != '\0') {
      // If the path component corresponds to a file in the directory, look it up
      vnode = vfs_getnode_from_directory(vnode, filename);
    }

    filename[0] = '\0'; // Clear filename
    path = end; // Optimisation for strchr, note that end points at the first character after the string
  }

  return vnode;
}

void
vfs_getpath(vfs_vnode_t *start, char *buffer, size_t size)
{
  int pos = 0;
  int total_len = 0;
  vfs_vnode_t *vnode = start;

  do
    {
      int len = strnlen (vnode->name, 64);

      if (vnode->type == VFS_MOUNT && vnode->parent == NULL)
        {
          vnode = vnode->parent;
          continue;
        }

      total_len += len + 1;

      vnode = vnode->parent;
    }
  while (vnode);

  if (total_len >= size)
    return;

  vnode = start;
  pos = total_len - 1;
  do
    {
      int len = strnlen (vnode->name, 64);

      if (vnode->type == VFS_MOUNT && vnode->parent == NULL)
        {
          vnode = vnode->parent;
          continue;
        }

      if (len > 0)
        {
          strlcpy (buffer + pos - len + 1, vnode->name, len + 1);
          buffer[pos + 1] = '/'; /* Tail will be zero-terminated below. */
          pos -= len;
        }
      buffer[pos--] = '/';

      vnode = vnode->parent;
    }
  while (vnode);

  buffer[total_len] = 0;
}
